#include "Win32LogOutput.h"
#include "Win32Window.h"

#include <d3d12.h>
#include <d3dcompiler.h>
#include <dxgi1_4.h>

#pragma comment (lib, "d3d12.lib")
#pragma comment (lib, "d3dcompiler.lib")
#pragma comment (lib, "dxgi.lib")

MY_LOG_LOCAL_SETUP("main", my::LogLevel::Verbose);


int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE prevInstance, _In_ LPSTR cmdLine, _In_ int showCmd)
{
	my::LogSystem::instance().add_output(my::make_ptr<my::WinLogOutput>());

	//MY_LOG_VERBOSE("hello %s!", "world");
	//MY_LOG_DEBUG("hello %s!", "world");
	//MY_LOG_INFO("hello %s!", "world");
	//MY_LOG_WARNING("hello %s!", "world");
	//MY_LOG_ERROR("hello %s!", "world");
	//MY_LOG_FATAL("hello %s!", "world");

	const int WINDOW_SIZE_X = 800;
	const int WINDOW_SIZE_Y = 600;
	const int FRAME_COUNT = 3;

	auto win = my::make_ptr<my::Win32Window>();
	win->get_event_handlers().OnDestroy = []() { PostQuitMessage(0); };
	win->Create(WINDOW_SIZE_X, WINDOW_SIZE_Y);

#if defined(_DEBUG)
	ID3D12Debug *debug;
	if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug))))
	{
		debug->EnableDebugLayer();
		// debug->Release();
	}
#endif

	IDXGIFactory4 *factory;
	if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))))
	{
		return 0;
	}

	IDXGIAdapter *adapter = nullptr;
	for (UINT i = 0; ; ++i)
	{
		if (SUCCEEDED(factory->EnumAdapters(i, &adapter)))
		{
			break;
		}
	}

	ID3D12Device *device;
	HRESULT hr = D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device));

	D3D12_COMMAND_QUEUE_DESC command_queue_desc = {};
	command_queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	command_queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;

	ID3D12CommandQueue *command_queue;
	hr = device->CreateCommandQueue(&command_queue_desc, IID_PPV_ARGS(&command_queue));

	DXGI_SWAP_CHAIN_DESC swap_chain_desc = {};
	swap_chain_desc.BufferCount = FRAME_COUNT;
	swap_chain_desc.BufferDesc.Width = WINDOW_SIZE_X;
	swap_chain_desc.BufferDesc.Height = WINDOW_SIZE_Y;
	swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
	swap_chain_desc.OutputWindow = win->get_handle();
	swap_chain_desc.SampleDesc.Count = 1;
	swap_chain_desc.Windowed = TRUE;

	IDXGISwapChain *swap_chain;
	hr = factory->CreateSwapChain(command_queue, &swap_chain_desc, &swap_chain);

	IDXGISwapChain3 *swap_chain_3;
	hr = swap_chain->QueryInterface(IID_PPV_ARGS(&swap_chain_3));

	// hr = factory->MakeWindowAssociation(win->get_handle(), DXGI_MWA_NO_ALT_ENTER);

	UINT frame_index = swap_chain_3->GetCurrentBackBufferIndex();

	D3D12_DESCRIPTOR_HEAP_DESC rtv_heap_desc = {};
	rtv_heap_desc.NumDescriptors = FRAME_COUNT;
	rtv_heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtv_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

	ID3D12DescriptorHeap *rtv_heap;
	hr = device->CreateDescriptorHeap(&rtv_heap_desc, IID_PPV_ARGS(&rtv_heap));

	UINT rtvDescriptorSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	ID3D12Resource *render_targets[FRAME_COUNT];
	for (UINT i = 0; i < FRAME_COUNT; ++i)
	{
		D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle = rtv_heap->GetCPUDescriptorHandleForHeapStart();
		rtv_handle.ptr = SIZE_T(INT64(rtv_handle.ptr) + INT64(rtvDescriptorSize) * i);
		hr = swap_chain_3->GetBuffer(i, IID_PPV_ARGS(&render_targets[i]));
		device->CreateRenderTargetView(render_targets[i], nullptr, rtv_handle);
	}

	ID3D12CommandAllocator *command_allocator;
	hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&command_allocator));








	D3D12_ROOT_SIGNATURE_DESC root_signature_desc = {};
	root_signature_desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;

	ID3DBlob *signature;
	ID3DBlob *error;
	hr = D3D12SerializeRootSignature(&root_signature_desc, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error);

	ID3D12RootSignature *root_signature;
	hr = device->CreateRootSignature(0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&root_signature));





	// Create the pipeline state, which includes compiling and loading shaders.
	ID3DBlob *vertexShader;
	ID3DBlob *pixelShader;
	ID3DBlob *errorMsg;

#if defined(_DEBUG)
	// Enable better shader debugging with the graphics debugging tools.
	UINT compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
	UINT compileFlags = 0;
#endif

	hr = D3DCompileFromFile(L"shader/simple.hlsl", nullptr, nullptr, "VSMain", "vs_5_0", compileFlags, 0, &vertexShader, &errorMsg);
	if (errorMsg)
	{
		const char *msg = (const char*)errorMsg->GetBufferPointer();
		MY_LOG_ERROR("error compile vertex shader: %s", msg);

		errorMsg->Release();
		errorMsg = nullptr;
	}

	hr = D3DCompileFromFile(L"shader/simple.hlsl", nullptr, nullptr, "PSMain", "ps_5_0", compileFlags, 0, &pixelShader, &errorMsg);
	if (errorMsg)
	{
		const char *msg = (const char*)errorMsg->GetBufferPointer();
		MY_LOG_ERROR("error compile pixel shader: %s", msg);

		errorMsg->Release();
		errorMsg = nullptr;
	}

	// Define the vertex input layout.
	D3D12_INPUT_ELEMENT_DESC inputElementDescs[] =
	{
		{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
		{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
	};

	// Describe and create the graphics pipeline state object (PSO).
	ID3D12PipelineState *pipeline_state;

	D3D12_GRAPHICS_PIPELINE_STATE_DESC pso_desc = {};
	pso_desc.InputLayout = { inputElementDescs, _countof(inputElementDescs) };
	pso_desc.pRootSignature = root_signature;
	pso_desc.VS = { vertexShader->GetBufferPointer(), vertexShader->GetBufferSize() };
	pso_desc.PS = { pixelShader->GetBufferPointer(), pixelShader->GetBufferSize() };

	pso_desc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
	pso_desc.RasterizerState.CullMode = D3D12_CULL_MODE_BACK;
	pso_desc.RasterizerState.FrontCounterClockwise = FALSE;
	pso_desc.RasterizerState.DepthBias = D3D12_DEFAULT_DEPTH_BIAS;
	pso_desc.RasterizerState.DepthBiasClamp = D3D12_DEFAULT_DEPTH_BIAS_CLAMP;
	pso_desc.RasterizerState.SlopeScaledDepthBias = D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS;
	pso_desc.RasterizerState.DepthClipEnable = TRUE;
	pso_desc.RasterizerState.MultisampleEnable = FALSE;
	pso_desc.RasterizerState.AntialiasedLineEnable = FALSE;
	pso_desc.RasterizerState.ForcedSampleCount = 0;
	pso_desc.RasterizerState.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF;

	pso_desc.BlendState.AlphaToCoverageEnable = FALSE;
	pso_desc.BlendState.IndependentBlendEnable = FALSE;
	const D3D12_RENDER_TARGET_BLEND_DESC defaultRenderTargetBlendDesc =
	{
		FALSE,FALSE,
		D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
		D3D12_BLEND_ONE, D3D12_BLEND_ZERO, D3D12_BLEND_OP_ADD,
		D3D12_LOGIC_OP_NOOP,
		D3D12_COLOR_WRITE_ENABLE_ALL,
	};
	for (UINT i = 0; i < D3D12_SIMULTANEOUS_RENDER_TARGET_COUNT; ++i)
		pso_desc.BlendState.RenderTarget[i] = defaultRenderTargetBlendDesc;

	pso_desc.DepthStencilState.DepthEnable = FALSE;
	pso_desc.DepthStencilState.StencilEnable = FALSE;
	pso_desc.SampleMask = UINT_MAX;
	pso_desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
	pso_desc.NumRenderTargets = 1;
	pso_desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
	pso_desc.SampleDesc.Count = 1;
	hr = device->CreateGraphicsPipelineState(&pso_desc, IID_PPV_ARGS(&pipeline_state));

	// Create the command list.
	ID3D12GraphicsCommandList *command_list;
	hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, command_allocator, pipeline_state, IID_PPV_ARGS(&command_list));

	// Command lists are created in the recording state, but there is nothing
	// to record yet. The main loop expects it to be closed, so close it now.
	hr = command_list->Close();

	float aspect_ratio = (float)WINDOW_SIZE_Y / (float)WINDOW_SIZE_X;

	ID3D12Resource *vertex_buffer;
	D3D12_VERTEX_BUFFER_VIEW vertex_buffer_view;

	// Create the vertex buffer.
	{
		struct Vertex
		{
			float position[3];
			float color[4];
		};

		// Define the geometry for a triangle.
		Vertex triangleVertices[] =
		{
			{ { 0.0f, 0.25f * aspect_ratio, 0.0f }, { 1.0f, 0.0f, 0.0f, 1.0f } },
			{ { 0.25f, -0.25f * aspect_ratio, 0.0f }, { 0.0f, 1.0f, 0.0f, 1.0f } },
			{ { -0.25f, -0.25f * aspect_ratio, 0.0f }, { 0.0f, 0.0f, 1.0f, 1.0f } }
		};

		const UINT vertexBufferSize = sizeof(triangleVertices);

		D3D12_RESOURCE_DESC vertex_buffer_desc;
		vertex_buffer_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
		vertex_buffer_desc.Alignment = 0;
		vertex_buffer_desc.Width = vertexBufferSize;
		vertex_buffer_desc.Height = 1;
		vertex_buffer_desc.DepthOrArraySize = 1;
		vertex_buffer_desc.MipLevels = 1;
		vertex_buffer_desc.Format = DXGI_FORMAT_UNKNOWN;
		vertex_buffer_desc.SampleDesc.Count = 1;
		vertex_buffer_desc.SampleDesc.Quality = 0;
		vertex_buffer_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
		vertex_buffer_desc.Flags = D3D12_RESOURCE_FLAG_NONE;

		D3D12_HEAP_PROPERTIES heap_properties;
		heap_properties.Type = D3D12_HEAP_TYPE_UPLOAD;
		heap_properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
		heap_properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
		heap_properties.CreationNodeMask = 1;
		heap_properties.VisibleNodeMask = 1;

		// Note: using upload heaps to transfer static data like vert buffers is not 
		// recommended. Every time the GPU needs it, the upload heap will be marshalled 
		// over. Please read up on Default Heap usage. An upload heap is used here for 
		// code simplicity and because there are very few verts to actually transfer.
		hr = device->CreateCommittedResource(
			&heap_properties,
			D3D12_HEAP_FLAG_NONE,
			&vertex_buffer_desc,
			D3D12_RESOURCE_STATE_GENERIC_READ,
			nullptr,
			IID_PPV_ARGS(&vertex_buffer));

		// Copy the triangle data to the vertex buffer.
		void* pVertexDataBegin;
		D3D12_RANGE readRange = { 0, 0 };        // We do not intend to read from this resource on the CPU.
		vertex_buffer->Map(0, &readRange, &pVertexDataBegin);
		memcpy(pVertexDataBegin, triangleVertices, sizeof(triangleVertices));
		vertex_buffer->Unmap(0, nullptr);

		// Initialize the vertex buffer view.
		vertex_buffer_view.BufferLocation = vertex_buffer->GetGPUVirtualAddress();
		vertex_buffer_view.StrideInBytes = sizeof(Vertex);
		vertex_buffer_view.SizeInBytes = vertexBufferSize;
	}

	// Create synchronization objects and wait until assets have been uploaded to the GPU.
	ID3D12Fence *fence;
	UINT64 fence_value;
	HANDLE fence_event;


	auto WaitForPreviousFrame = [&]()
	{
		const UINT64 last_frame_fence_value = fence_value;
		command_queue->Signal(fence, last_frame_fence_value);
		++fence_value;

		// Wait until the previous frame is finished.
		if (fence->GetCompletedValue() < last_frame_fence_value)
		{
			hr = fence->SetEventOnCompletion(last_frame_fence_value, fence_event);
			WaitForSingleObject(fence_event, INFINITE);
		}

		frame_index = swap_chain_3->GetCurrentBackBufferIndex();
	};

	{
		hr = device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence));
		fence_value = 1;

		// Create an event handle to use for frame synchronization.
		fence_event = CreateEvent(nullptr, FALSE, FALSE, nullptr);
		if (fence_event == nullptr)
		{
			// ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
		}

		// Wait for the command list to execute; we are reusing the same command 
		// list in our main loop but for now, we just want to wait for setup to 
		// complete before continuing.
		WaitForPreviousFrame();
	}

	D3D12_VIEWPORT viewport;
	viewport.TopLeftX = 0.0f;
	viewport.TopLeftY = 0.0f;
	viewport.Width = (float)WINDOW_SIZE_X;
	viewport.Height = (float)WINDOW_SIZE_Y;
	viewport.MinDepth = D3D12_MIN_DEPTH;
	viewport.MaxDepth = D3D12_MAX_DEPTH;

	D3D12_RECT scissor_rect;
	scissor_rect.left = 0;
	scissor_rect.top = 0;
	scissor_rect.right = WINDOW_SIZE_X;
	scissor_rect.bottom = WINDOW_SIZE_Y;

	auto PopulateCommandList = [&]()
	{
		// Command list allocators can only be reset when the associated 
		// command lists have finished execution on the GPU; apps should use 
		// fences to determine GPU execution progress.
		hr = command_allocator->Reset();

		// However, when ExecuteCommandList() is called on a particular command 
		// list, that command list can then be reset at any time and must be before 
		// re-recording.
		hr = command_list->Reset(command_allocator, pipeline_state);

		// Set necessary state.
		command_list->SetGraphicsRootSignature(root_signature);
		command_list->RSSetViewports(1, &viewport);
		command_list->RSSetScissorRects(1, &scissor_rect);

		// Indicate that the back buffer will be used as a render target.
		D3D12_RESOURCE_BARRIER barrier_1 = {};
		barrier_1.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
		barrier_1.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
		barrier_1.Transition.pResource = render_targets[frame_index];
		barrier_1.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
		barrier_1.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
		barrier_1.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
		command_list->ResourceBarrier(1, &barrier_1);

		D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle0;
		rtvHandle0.ptr = SIZE_T(INT64(rtv_heap->GetCPUDescriptorHandleForHeapStart().ptr) + INT64(frame_index) * INT64(rtvDescriptorSize));
		command_list->OMSetRenderTargets(1, &rtvHandle0, FALSE, nullptr);

		// Record commands.
		const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
		command_list->SetPipelineState(pipeline_state);
		command_list->ClearRenderTargetView(rtvHandle0, clearColor, 0, nullptr);
		command_list->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
		command_list->IASetVertexBuffers(0, 1, &vertex_buffer_view);
		command_list->DrawInstanced(3, 1, 0, 0);

		// Indicate that the back buffer will now be used to present.
		D3D12_RESOURCE_BARRIER barrier_2 = {};
		barrier_2.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
		barrier_2.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
		barrier_2.Transition.pResource = render_targets[frame_index];
		barrier_2.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
		barrier_2.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
		barrier_2.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
		command_list->ResourceBarrier(1, &barrier_2);

		hr = command_list->Close();
	};

	auto OnRender = [&]()
	{
		// Record all the commands we need to render the scene into the command list.
		PopulateCommandList();

		// Execute the command list.
		ID3D12CommandList* ppCommandLists[] = { command_list };
		command_queue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

		// Present the frame.
		hr = swap_chain_3->Present(1, 0);

		WaitForPreviousFrame();
	};

	auto OnDestroy = [&]()
	{
		WaitForPreviousFrame();
		CloseHandle(fence_event);
	};

	MSG msg;
	for (;;)
	{
		while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
		{
			if (msg.message == WM_QUIT)
			{
				return (int)msg.wParam;
			}

			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}

		OnRender();
	}

	OnDestroy();

	return 0;
}
